"""
.. module:: uri

This module contains all Uri classes and declarations. A Uri is the fundamental
identifier used for metadata and for identification of Resources within the
framework. It allows suitable constructed identifiers to be used with little 
fear of namespace clashes. Importantly, *every* Uri is defined in a 
configuration resource, which gives any user of this framework a good quality
internal data dictionary.

There is a role for identifiers within a Resource, but simple python identifiers
often lead to easier to read code. In many cases, these identifiers are also 
described in the configuration resources.

Uri has a class variable to hold namespace identifiers. A change to this table  
affects every Uri in the system. Namespace identifiers are used to provide a 
more compact, easier to read Uri. It is intended that a system might only add 
one or two namespace identifiers. It is important to note, that namespace 
identifiers essentially counter the benefit of Uri clashes in the Uri space, 
because its easier for a namespace identifier called 'fred:' in on part of an 
application to clash with a similarly named but different name space identifier 
used somewhere else.

*Constants*:

.. seealso::

    Uniform Resource Identifier (URI): Generic Syntax - 
    http://www.ietf.org/rfc/rfc3986.txt
    
    A URI can be classified as a locator, a name, or both. A "Uniform Resource
    Locator" (URL) refers to the subset of URIs that, in addition to identifying
    a resource, provide a means of locating the resource by describing its
    primary access mechanism (e.g., its network "location").

    A common misunderstanding of URIs is that they are only used to refer to 
    accessible resources.  The URI itself only provides identification; access 
    to the resource is neither guaranteed nor implied by the presence of a URI.
    
    Namespaces in XML 1.0 - http://www.w3.org/TR/REC-xml-names/
"""

import logging
from urlparse import urlparse
import hashlib

class Uri(object):
    """
    This class simplifies the construction and manipulation of URI's. Each
    Uri has a base and a relative part. The base includes the namespace.
    
    A Uri may be created with a string, another Uri, or as an empty Uri::
    
        uri1 = Uri('internal:test/abc')
        uri2 = Uri(uri1)
        uri3 = Uri()
        
    Text may be appended to a uri using the '+' operator::
    
        uri1 = Uri('internal:test')
        uri2 = uri1 + 'abc'
        
    Text may also be appended to a uri using a '.' attribute::
    
        uri1 = Uri('internal:test')
        uri2 = uri1.abc
        
    The full Uri (with namespaces expansion) can be obtained using full()::
    
        uri1 = Uri('internal:test/abc')
        uri1.full() == Uri('http://node.internal/test/abc')
        
    A copy of a Uri can be returned. It is a new instance that will test as 
    equal to the first uri::
    
        uri1 = Uri('internal:test/abc')
        uri2 = uri1.copy()
        uri1 == uri2
        
    A string representation of a string is available::
    
        uri1 = Uri('internal:test/abc')
        str(uri1) == 'internal:test/abc'
        
    A Uri with no namespace specified can be shortened to the uri with a 
    namespace::
    
        uri1 = Uri('http://node.internal/test/abc')
        uri1.shorten() == Uri('internal:test/abc')
        
    A Uri that contains a fragment may also be split::
    
        uri1 = Uri('http://node.internal/test/abc#xyz')
        (Uri('http://node.internal/test/abc'), 'xyz') == uri1.fragmented()
        uri1.uri() == Uri('http://node.internal/test/abc')
        uri1.fragment() == 'xyz'
        
    A Parsed URL can be returned using the url() method::
    
        uri1 = Uri('http://node.internal/test/abc#xyz')
    
    Create a new Uri from a string, which may include a namespace prefix.
                
    **Constants**:
    
    .. data:: namespaces
        
        namespaces is a dictionary of namespaces.
    
    :param uri: A string that will be made into a Uri. Leading and trailing 
        whitespace are removed.
    """
    
    # This is updated dynamically as needed, usually during node start-up.
    # These *MUST* end with a slash character.
    namespaces = {
        'term':     'http://node.internal/data_dictionary/',
        'dc':       'http://purl.org/dc/elements/1.1/',
        'internal': 'http://node.internal/'
        }

    @classmethod
    def add_namespace(cls, ns, uri):
        """
        This should be used sparingly. Node initialisation adds a couple of
        namespace identifiers stored in configuration files.
        
        Example::
        
            Uri.add_namespace('fred', 'http://node.internal/fred/')
            u = Uri('fred:abc').full()
            u -> Uri('http://node.internal/fred/abc')
            
        :param ns: String - Namespace key
        :param uri: String or Uri - Namespace Uri with a trailing '/'.
        """
        ns_uri = str(uri)
        if ns_uri.endswith('/'):
            cls.namespaces[ns] = ns_uri
        else:
            raise ValueError("New namespace uri (%s) must end in a '/'" % \
                             ns_uri)
        
    def __init__(self, uri=None):
        #self.logger = logging.getLogger('yarra.datastore.uri.Uri')
        
        # self._uri is always a string. It may be None, an empty string, or a 
        # uri string with or without a namespace prefix.
        if uri is None:
            self._uri = None
        else:
            self._uri = str(uri).strip()

    def add(self, name):
        """
        This function adds the argument 'name' (with any whitespace removed) to 
        the end of this URI. A slash is added before 'name' if needed.
        
        If the current uri is None or an empty string, 'name' becomes the uri.
        
        Example::
        
            u = Uri('term:abc')
            u.add('fred')
            u -> Uri('term:abc/fred')
        
        :param name: string or any type that supports str().
        :rtype: *this* Uri with the extra text added. Not normally used.
        """  
        if self._uri is None:
            self._uri = str(name).strip()
        elif self._uri == '':
            self._uri = str(name).strip()
        else:
            # Note: endswith() is not used because it doesn't work with
            # __getattr__
            last_char = self._full()[-1]
            if last_char == '/':
                self._uri += str(name).strip()
            elif last_char == ':':       # a namespace string               
                self._uri += str(name).strip()
            else:
                self._uri += '/' + str(name).strip()
        return self

    def copy(self):
        """
        This function is a copy constructor. 
        
        :rtype: Uri - a *copy* of this Uri.
        """
        return Uri(self._uri)

    def collection(self, coll=True):
        """
        This function turns the current URI instance into a collection 
        ('coll' = True or None) or out of a collection ('coll' = False). 
        A collection is created by appending a slash (if there isn't already a 
        trailing slash.
        
        An empty Uri always returns a new empty Uri.
        
        Example::
        
            u = Uri('term:abc').collection(True)
            u -> Uri('term:abc/')
        
            u = Uri('term:abc/def/ghi/').collection(False)
            u -> Uri('term:abc/def/ghi')
        
        :param coll: Boolean - True (or None) if current Uri instance is to be 
            turned into a collection.
        :rtype: Uri - a *copy* of this Uri with the new collection state
        """
        if self._uri is None:
            return Uri()
        
        # No change needed if you want to convert to/from collection when the 
        # uri is already is/not collection
        if coll == self.is_collection():
            return self.copy()
        
        if coll: # make a collection
            new_uri = self._uri + '/'
        else: # remove trailing slash
            if (self._uri.endswith(':') and \
                    (self._uri[:-1] in Uri.namespaces.keys())):
                new_uri = self._full()[:-1]
            else:
                new_uri = self._uri[:-1]
        return Uri(new_uri)

    def is_collection(self):
        """
        This function returns True if the current instance of Uri is a 
        collection (i.e. it ends with a slash '/').
        
        :rtype: Boolean - True if *this* Uri is a collection.
        """
        return self._full().endswith('/')
      
    def split(self):
        """
        This function will split a uri into a list of its parts.
        ''.join(list) will return the original uri.
        
        For the schemes http, https, ftp and file:
            - the first element is the scheme, domain and port, 
              e.g. 'http://aaa.com:80/'
            - the path is split on slash and each element is terminated by 
              that slash.
            - URL fragments are supported.
            
        For the scheme urn:
            - the URI is split on colon.
            
        For the scheme mailto:
            - the URI is split into mailto: and the rest of the string.
            
        If there is no scheme, the path is split on slash.
        If the scheme is unknown, it is terminated with a colon and split on a 
        slash.
        
        If the the joined output and the original string are not the same, the
        original string is returned. The exception has been removed.
        
        uri_test.py contains many examples of this functionality.

        :rtype: None or List - list of Uri parts.
        """
        if self._uri is None:
            return None
        else:
            uri = self._full()
            bits = urlparse(uri, '', True)
            #print('Processing URI: %s (s:%s n:%s p:%s f:%s)' % \
            #    (uri, bits.scheme, bits.netloc, bits.path, bits.fragment))
            # the simplest case
            if uri == '':
                return None
            # handle the base (scheme + domain + port) and set the split_char
            if bits.scheme in ['http', 'https', 'ftp', 'file']:
                split_char = '/'          
                base = bits.scheme + '://' + bits.netloc
                path = bits.path
                #print('Scheme base: %s' % base)
                new_path = [base]
            elif bits.scheme in ['urn']:
                split_char = ':'
                base = bits.scheme + split_char
                #print('Scheme (urn): %s' % base)
                path = bits.path
                new_path = [base]
            elif bits.scheme in ['mailto']:
                split_char = None
                #print('Scheme (mailto): %s' % bits.path)
                new_path = ['mailto:', bits.path]
            elif bits.scheme == '':
                split_char = '/'
                #print('No scheme.')
                new_path = []
                path = uri
            else:
                # just do the best we can
                split_char = '/'
                #print('Unsupported scheme: %s' % bits.scheme)
                new_path = [bits.scheme + ':']
                path = bits.path
            # handle the path with a split_char of slash or colon.
            if split_char is not None:
                # handle the case where split_char should be part of base,
                # in path_parts[0], not the first character in the path.
                if (len(new_path) > 0) and \
                        (not new_path[0].endswith(split_char)) and \
                        (path.startswith(split_char)):
                    new_path[0] += split_char
                    path = path[1:]
                #
                path_parts = path.split(split_char)
                #print('Path: %s' % repr(path_parts))
                for p in path_parts[:-1]:
                    #print('Path part: %s' % p + split_char)
                    new_path.append(p + split_char)
                if path_parts[-1] != '':
                    #print('Path part: %s' % path_parts[-1])
                    new_path.append(path_parts[-1])
            # handle a fragment
            if bits.fragment != '':
                new_path.append('#' + bits.fragment)
            # check that we can still join it up as the original URI
            if ''.join(new_path).lower() == uri.lower():
                return new_path
            else:
                # Return the uri if it can't be successfully split and joined.
                return [uri]
        
    def fragmented(self):
        """
        This function returns this uri as a tuple consisting of the Uri 
        without the fragment, and the fragment as a string (or None if there
        was no fragment).
        
        Example::
        
            u = Uri('term:abc#frag').fragmented()
            u -> ('term:abc/', 'frag')
 
        :rtype: Tuple as (Uri, string) or (Uri, None).
        """
        if self._uri is None:
            return (None, None)
        else:
            bits = self._uri.split('#')
            if len(bits) == 2:
                return (Uri(bits[0]), bits[1])
            else:
                return (Uri(bits[0]), None)
        
    def uri(self):
        """
        This function returns this uri as a Uri without any fragment.
        
        Example::
        
            u = Uri('term:abc#frag').uri()
            u -> Uri('term:abc/')
        
        :rtype: Uri without fragment
        """
        return self.fragmented()[0]
        
    def fragment(self):
        """
        This function returns the fragment portion of this Uri.
        
        Example::
        
            u = Uri('term:abc#frag').fragment()
            u -> 'frag'
        
        :rtype: String - fragment (without the '#' character).
        """
        return self.fragmented()[1]
        
    def url(self):
        """
        This function parses the current instance of Uri as a URL and returns
        a ParseResult tuple representation.
        
        Example::
        
            u = Uri('term:abc#frag').url()
            u -> ParseResult(scheme='http', netloc='node.internal', 
                 path='/data_dictionary/abc', params='', query='', 
                 fragment='frag')
        
        :rtype: URL ParseResult tuple.
        """
        return urlparse(self._full())
  
    def full(self):
        """
        This function returns a Uri with the namespace, if any expanded.
        
        Example::
        
            u = Uri('term:abc')
            u ->  'http://node.internal/data_dictionary/abc'
            u = Uri()
            u ->  None
                    
        :rtype: None, or an expanded *copy* of the Uri as a Uri
        """
        return Uri(self._full())
  
    def _full(self):
        """
        This internal function returns a Uri with the namespace, 
        if any expanded.
        
        :rtype: None, or an expanded copy of the Uri as a string
        """
        if self._uri is None:
            return None
        else:
            if ':' in self._uri:
                parts = self._uri.split(':', 1)
                if parts[0] in Uri.namespaces.keys():
                    parts[0] = str(Uri.namespaces[parts[0]])
                    return ''.join(parts)
                else:
                    return self._uri
            else:
                return self._uri

    def shorten(self):
        """
        This function takes a Uri and attempts to apply the longest matching
        Uri namespace to it.
        
        Example::
        
            u = Uri('http://node.internal/data_dictionary/abc').shorten()
            u ->  Uri('term:abc')
            u = Uri().shorten()
            u ->  None
        
        :rtype: None or a *copy* of this Uri after it has been shortened.
        """
        uri = self._uri
        if uri is None:
            return None
        else:
            # invert the Uri.namespaces dictionary
            inverted = dict([[v,k] for k,v in Uri.namespaces.items()])
            # sort and reverse the dictionary so that we match the most specific
            # namespace first.
            inverted_keys = sorted(inverted.keys())
            inverted_keys.reverse()
            for key in inverted_keys:
                ns = inverted[key]
                if uri.startswith(key):
                    return Uri(ns + ':' + uri[len(key):])
            return self.copy()
 
    def __str__(self):
        """
        This function returns the string representation of this uri with its 
        namespace intact.
        
        This function is called by Python's str().
        
        Example::
        
            u = str(Uri('term:abc'))
            u ->  'term:abc'
            u = str(Uri())
            u ->  ''
        
        :rtype: string
        """
        if self._uri is None:
            return ''
        else:
            return self._uri
 
    def __repr__(self):
        """
        This function returns the repr representation of this uri. If used in an
        eval() expression, a Uri object will be created.
        
        This function is called by Python's str().
        
        Example::
        
            u = repr(Uri('term:abc'))
            u ->  'Uri('term:abc')'
            u = repr(Uri())
            u ->  'Uri()'
        
        :rtype: string able to be exec'd back into a Uri.
        """
        if self._uri is None:
            return "Uri()"
        else:
            return "Uri('%s')" % self._uri

    def __len__(self):
        """
        This function returns the length of the full Uri string. 
        
        This function is called by Python's len().

        Example::
        
            u = len(Uri('term:abc'))
            u ->  40
            u = len(Uri())
            u ->  0
        
        :rtype: int length of *this* full uri.
        """
        if self._uri is None:
            return 0
        else:
            return len(self._full())

    def __cmp__(self, other):
        """
        This function performs a string comparison of the current instance's 
        full Uri with the full representation of Uri 'other'. It should return 
        a negative integer if self < other, zero if self == other, a positive 
        integer if self > other.
        
        This function is called when comparing the current Uri instance with
        'other'.
        
        Example::
        
            this = Uri('term:this')
            c = cmp(this, Uri('term:that'))
            c -> 1
            
            this = Uri('term:this')
            c = cmp(this, Uri('term:this'))
            c -> 0
            
            this = Uri('term:this')
            c = cmp(this, Uri('term:zzzz'))
            c -> -1
        
        :param other: Uri
        :rtype: int
        """
        s = self._full()
        if type(other) == type(Uri()):
            o = other._full()
        else:
            o = str(other)
        return cmp(s, o)

    def __hash__(self):
        """
        Returns a hash for this uri. If an object supports this method, then 
        it may be used as a key in a dictionary.
        
        Example::
        
            u = hash(Uri('term:abc'))
            u -> -793169541
        
        :rtype: int hash (in the signed range -2^16 to +2^16)
        """
        return int(hashlib.md5(str(self.full())).hexdigest(), 16)
    
    def __add__(self, name):
        """
        This function returns a new Uri with 'name' appended. This uri is not
        changed. The parameter can be a Uri or any type that supports str().
        
        This function is called by Python when the Uri instance 'other' is being 
        added to the current instance with the '+' operator.
        
        Note: The current instance is NOT effected by this function.
        Note: Whitespace is removed.
        
        Example::
        
            u = Uri('term:abc') + 'def'
            u -> Uri('term:abc/def')
       
        :param other: Uri or any type that supports str()
        :rtype: A new Uri with the text attached.
        """
        return self.copy().add(name)

    def __getattr__(self, name):
        """
        This function allows the "." notation of Uri's. This is called when
        an attribute lookup has not found the attribute in the usual places
        (i.e. it is not an instance attribute nor is it found in the class tree  
        for self). Here we use it to add the attribute 'name' as the trailing  
        path element in the current instance of Uri.
        
        Example::
        
            u = Uri('term:abc').def
            u -> Uri('term:abc/def')
        
        :param name: string - Uri path name
        :rtype: Uri
        """
        return self.copy().add(name)
